package com.nn.algorithm;

import org.apache.commons.math3.linear.MatrixUtils;
import org.apache.commons.math3.linear.RealMatrix;

public class BackPropagationLayer {

	private Layer layer;
	private double[] error;
	private double[] errorDelta;
	private BackPropagationLayer previous;
	private RealMatrix accumulationMatrix;
	private RealMatrix accumulationMatrixDelta;
	
	public BackPropagationLayer(Layer layer, BackPropagationLayer previous) {
		this.layer = layer;
		this.error = new double[layer.getNumberOfNeurons()];
		this.errorDelta = new double[layer.getNumberOfNeurons()];
		this.setPrevious(previous);
		init();
	}

	private void init() {

		if (this.layer.getLayerType() != LayerType.OUTPUT) {

			int rows = this.layer.getNumberOfNeurons();
			int columns = this.layer.getNext().getNumberOfNeurons();
			System.out.println("Rows=" + rows);
			System.out.println("Columns=" + columns);
			this.accumulationMatrix = MatrixUtils.createRealMatrix(rows + 1,
					columns);
			this.accumulationMatrixDelta = MatrixUtils.createRealMatrix(
					rows + 1, columns);
		}
	}

	public void clearError() {
		for (int i = 0; i < error.length; i++) {
			error[i] = 0;
		}
	}
	
	public void calculateError(double[] idealOutput) {
		if (this.layer.getLayerType() == LayerType.OUTPUT) {
			// calculate error for outpur layer
			calculateOutputLayerErrors(idealOutput);
		} else {
			// calculate errors for the input and hidden layers
			calculateOutputForOtherLayers();
		}
	}

	private void calculateOutputForOtherLayers() {
		int rows = this.layer.getNumberOfNeurons();
		int columns = this.layer.getNext().getNumberOfNeurons();
		for(int i=0; i<columns; i++)
		{
			for(int j=0; j<rows; j++)
			{
				double temp= getPrevious().getErrorDelta(i) * this.layer.getInput()[j];
				this.accumulationMatrixDelta.addToEntry(j, i, temp);
				error[j] += this.layer.getWeightMatrix().getEntry(j, i) * getPrevious().getErrorDelta(i);
			}
			this.accumulationMatrixDelta.addToEntry(rows, i, getPrevious().getErrorDelta(i));//for the added threshold values in the weight matrix (input would be 1 so it doesn't count)
		}
		
		if(this.layer.getLayerType() == LayerType.HIDDEN)
		{
			for(int j=0; j<rows; j++)
			{
				errorDelta[j] = error[j] * Utils.derivativeFunction(layer.getInput()[j]); //error * sigmoid derivative function
			}
		}
		
	}
	
	private double getErrorDelta(int column)
	{
		return errorDelta[column];
	}

	private void calculateOutputLayerErrors(double[] idealOutput) {
		
		for(int i=0; i<layer.getNumberOfNeurons(); i++)
		{
			error[i] = idealOutput[i] - layer.getInput()[i];
			errorDelta[i] = error[i] * Utils.derivativeFunction(layer.getInput()[i]); //error * sigmoid derivative function
		}
		
	}
	
	public void learn(double learnRate, double momentum)
	{
		if(this.layer.getLayerType() != LayerType.OUTPUT)
		{
			RealMatrix m1 = accumulationMatrixDelta.scalarMultiply(learnRate);
			RealMatrix m2 = accumulationMatrix.scalarMultiply(momentum);
			accumulationMatrix = m1.add(m2);
			this.layer.setWeightMatrix(this.layer.getWeightMatrix().add(accumulationMatrix));
			
			//clear delta matrix
			int rows = this.layer.getNumberOfNeurons();
			int columns = this.layer.getNext().getNumberOfNeurons();
			this.accumulationMatrixDelta = MatrixUtils.createRealMatrix(rows + 1, columns);
		}
	}

	public BackPropagationLayer getPrevious() {
		return previous;
	}

	public void setPrevious(BackPropagationLayer previous) {
		this.previous = previous;
	}

}
